package il.technion.cs236369.webserver;

import il.technion.cs236369.webserver.typeHandlers.TypeHandler;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.charset.Charset;
import java.util.Properties;

import org.json.JSONException;
import org.json.JSONObject;
import org.json.XML;

import org.apache.http.Header;
import org.apache.http.HttpException;
import org.apache.http.HttpResponse;
import org.apache.http.ProtocolVersion;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.DefaultHttpServerConnection;
import org.apache.http.message.BasicHttpResponse;


/**
 * Thread that reads requests from the requests queue and serves them
 * @author Yuval and Eddie
 *
 */
public class RequestHandlerThread extends Thread {

	private WebServer webServer;    //this is received from the webServer itself
	private HttpResponse response;
	
	public RequestHandlerThread() {
		super();
		response = null;
	}

	/**
	 * Take a request from the queue and serve it
	 */
	@Override
	public void run() {
		while(true) {
			//take a request from the queue. blocks if there is no request in the queue
			Request request = webServer.takeRequestFromQueue();
			String extention = request.getRequestExtension();
			String method = request.getMethod();
			File requested = new File(request.getAbsolutePath());
			
			// Using else-if we ensure that once an error occurred, the request won't be handled anymore
			
			//we handle only get and post requests
			if (!method.equalsIgnoreCase("GET") && !method.equalsIgnoreCase("POST")) {
				request.getWebServer().sendErrorResponse(request.getConnection(), 400,
						"Bad Request", "This server handles only GET and POST requests",true);
				return;
			}
			
			//check that file exists and that request is not for root directory
			else if ( (!requested.exists() || requested.isDirectory() == true) &&
					!request.getAbsolutePath().equals(webServer.getBaseDir()) ) {
				request.getWebServer().sendErrorResponse(request.getConnection(), 404,
						"Not Found", "The requested page doesn't exists.",true);
				return;
			} 
			
			// GET or POST and file exists. check if static or dynamic request
			ServerHandler dynamicPage = webServer.supportedTypes.getServerHandler(extention);
			if (dynamicPage != null) { // the request is for a dynamic page
				runDynamicPage(dynamicPage, request);
			} else { // static page
				
				// verify it's a GET request (static pages don't support POST requests)
				if (method.equalsIgnoreCase("GET")) { 
					getStaticPage(request);
					
					// a successful fetch of a static file
					if (response!=null) {	
						DefaultHttpServerConnection connection = request.getConnection();
						try {
							connection.sendResponseHeader(response);
							connection.sendResponseEntity(response);
							connection.flush();
							connection.shutdown();
						} catch (HttpException e) {
							e.printStackTrace();
							webServer.sendErrorResponse(connection, 500, "Internal Server Error", 
									"Static file exists, but an error occurred while generating static response",true);
						} catch (IOException e) {
							e.printStackTrace();
							webServer.sendErrorResponse(connection, 500, "Internal Server Error", 
									"Static file exists, but an error occurred while generating static response",true);
						}
						
					}
					
				} else { // POST requests can only be handled by type handler. response error.
					request.getWebServer().sendErrorResponse(request.getConnection(), 400,
							"Bad Request", "Post requests need a type handler and this server doesn't have a handler for requested file type",true);
				}
			}
		}
	}

	public void setWebServer(WebServer webserver) {
		this.webServer = webserver;
	}

	private void runDynamicPage (ServerHandler handler, Request request) {
		Properties props = handler.getProperties();
		Class<? extends TypeHandler> handlerClass = null;
		try {
			handlerClass = handler.getHandlerClass();
			Constructor<? extends TypeHandler> ccc = (Constructor<? extends TypeHandler>) handlerClass.getConstructor(props.getClass());
			TypeHandler typehandler = (TypeHandler) ccc.newInstance(props);
			typehandler.handleRequest(request);
		} catch (InstantiationException e) {
			e.printStackTrace();
			request.getWebServer().sendErrorResponse(request.getConnection(), 500,
					"Internal Server Error", "Instantiation error",true);
		} catch (NoSuchMethodException e) {
			request.getWebServer().sendErrorResponse(request.getConnection(), 500,
					"Internal Server Error", "Dynamic class not found",true);
			e.printStackTrace();
		} catch (SecurityException e) {
			request.getWebServer().sendErrorResponse(request.getConnection(), 500,
					"Internal Server Error", "Security issue getting dynamic method",true);
		} catch (IllegalAccessException e) {
			e.printStackTrace();
			request.getWebServer().sendErrorResponse(request.getConnection(), 500,
					"Internal Server Error", "Illegal access",true);
		} catch (IllegalArgumentException e) {
			e.printStackTrace();
			request.getWebServer().sendErrorResponse(request.getConnection(), 500,
					"Internal Server Error", "Illegal argument when invoking dynamic method",true);
		} catch (InvocationTargetException e) {
			request.getWebServer().sendErrorResponse(request.getConnection(), 500,
					"Internal Server Error", "Dynamic method run failure",true);
			e.printStackTrace();
		} catch (ClassNotFoundException e) {
			request.getWebServer().sendErrorResponse(request.getConnection(), 500,
					"Internal Server Error", "TSP class not found",true);
		}
	}
	
	private void getStaticPage(Request request) {
		try {
			String requestedFile = null;
			//check if need to convert xml to json
			boolean convertToJason = checkXmlJsonHeader(request);
			if(!convertToJason) {
				requestedFile = readFile(request.getAbsolutePath());
			} else {
				if(request.getRequestExtension().equalsIgnoreCase("xml")) {
					requestedFile = convertXmlToJson(request);
				} else {
					//return error- convert header but not xml file request
					request.getWebServer().sendErrorResponse(request.getConnection(), 400,
							"Bad Request", "This server can only convert XML files. Request contained Convert header, but file is not XML file",true);
					return;
				}
			}
			
			String mime = webServer.supportedTypes.getMimeType(request.getRequestExtension());
			if (convertToJason) {
				mime = webServer.supportedTypes.getMimeType("json");
			}
			if (mime == null) {
				mime = "text/html";
			}
			response = new BasicHttpResponse(new ProtocolVersion("HTTP", 1, 1), 200, "OK");
			response.addHeader("Date", WebServer.generateServerDate());
			response.addHeader("Content-Type", mime);
			response.addHeader("Connection", "close");
			StringEntity entity = new StringEntity(requestedFile);
			response.setEntity(entity);
		} catch (UnsupportedEncodingException e) {
			request.getWebServer().sendErrorResponse(request.getConnection(), 500,
					"Internal Server Error", "The Character Encoding is not supported", true);
			e.printStackTrace();
		} catch (IOException e) {
			request.getWebServer().sendErrorResponse(request.getConnection(), 500,
					"Internal Server Error", "server's I\\O Error", true);
			e.printStackTrace();
		}
		response.addHeader("Content-Length", response.getEntity().getContentLength() + "");
	}
	
	
	private static String readFile(String path) throws IOException {
		FileInputStream stream = new FileInputStream(new File(path));
		try {
			FileChannel fc = stream.getChannel();
			MappedByteBuffer bb = fc.map(FileChannel.MapMode.READ_ONLY, 0, fc.size());
			/* Instead of using default, pass in a decoder. */
			return Charset.defaultCharset().decode(bb).toString();
		}
		finally {
			stream.close();
		}
	}
	
	/*
	 * checks if the header Convert: xml/json exists in the request
	 */
	private boolean checkXmlJsonHeader(Request request) {
		Header[] requestHeaders = request.getHeaders();
		
		for(Header h: requestHeaders) {
			if(h.getName().equalsIgnoreCase("Convert") && h.getValue().equalsIgnoreCase("xml/json")) {
				return true;
			}
		}
		return false;
	}
	
	/*
	 * converts the requested xml file to json and fill request entity
	 */
	private String convertXmlToJson(Request request) throws IOException {
		String xmlFile = readFile(request.getAbsolutePath());
		String jsonFile = null;
		try {
			JSONObject xmlJSONObj = XML.toJSONObject(xmlFile);
			jsonFile = xmlJSONObj.toString(0);
		} catch (JSONException e) {
			request.getWebServer().sendErrorResponse(request.getConnection(), 500,
					"Internal Server Error", "illigale XML file to convert",true);
		}
		return jsonFile;
	}
}
